<?php
require_once "JKSocket.php";
require_once "JKWebSocketArgument.php";
/**
 * Class JKSocket
 *
 * @method triggerHandshake($argument=[])
 * @method triggerBeforeHandshake($argument=[])
 *
 * @method offHandshake($callbale)
 * @method offBeforeHandshake($callbale)
 *
 * @method onHandshake($callbale)
 * @method onBeforeHandshake($callbale)
 */
class JKWebsocket extends JKSocket
{
    const HANDSHAKE_KEY_NAME = '__HANDSHAKE__';
    const WS_OPCODE_CONTINUE = 0x0;
    const WS_OPCODE_TEXT = 0x1;
    const WS_OPCODE_BIN = 0x2;
    const WS_OPCODE_CLOSE = 0x8;
    const WS_OPCODE_PING = 0x9;
    const WS_OPCODE_PONG = 0xA;
    
    protected $_argumentClass = 'JKWebSocketArgument';
    
    public function __construct($port=null, $addr='127.0.0.1') {
        parent::__construct($port, $addr);
        $this->config('recv_mode', self::RECV_MODE_BIN);
    }
    
    public function listen()
    {
        //
        // 处理websocket的消息
        //
        array_unshift($this->_callables['recv'], function(self $obj, JKWebSocketArgument $event) {
            /** @var $client self */
            $client = $event->getClient();
            $data = $event->getData();
            /** @var JKWebSocketArgument $argm */
            $argm = $this->_createArgument();
            $argm->setClient($client);
            
            //客户端还没握手, 进行握手
            if(!$client->get(self::HANDSHAKE_KEY_NAME)) {
                $this->triggerBeforeHandshake($argm);
                $client->enableKey(self::HANDSHAKE_KEY_NAME);
            
                $requestData = $this->_parseHeaderString($data);
            
                //没有指定的SEC-WEBSOCKET-KEY, 强制关闭连接
                if(empty($requestData['header']['SEC-WEBSOCKET-KEY'])) {
                    $client->send('Missing SEC-WEBSOCKET-KEY');
                    $obj->closeClient($client);
                    $event->stopPropagation();
                    return;
                }
            
                $handshakeResponse = $this->_generateHandshakeResponse($requestData['header']['SEC-WEBSOCKET-KEY']);
                $client->sendRaw($handshakeResponse);
                echo "handshake with Client #{$client->getIndex()}({$client->getAddr()})\n";
                $event->stopPropagation();
                $this->triggerHandshake($argm);
                return;
            }
        
            // if($client->get('FRAME_CONTINUE')) {
            //     $frameDetail = $client->get('FRAME_INFO');
            //     $frameDetail['data'] = $client->buffer . $data;
            //     $client->buffer = null;
            //     $client->set('FRAME_INFO', null);
            //     $client->set('FRAME_CONTINUE', false);
            // } else {
                $frameDetail = $this->_parseFrame($data);
            // }
            //解析websocket帧
            
            $frameInfo = $frameDetail;
            unset($frameInfo['data']);
            
            $event->setFrameInfo($frameInfo);
    
            // if(0 == $frameDetail['fin']) {
            //     $client->buffer = $frameDetail['data'];
            //     $client->set('FRAME_INFO', $frameInfo);
            //     $client->set('FRAME_CONTINUE', true);
            //     $event->stopPropagation();
            //     return;
            // }
            
            $unmaskData = $this->unmaskFrameData($frameDetail['data'], $frameDetail['mask'], $frameDetail['masking_key']);
            
            switch ($frameDetail['opcode']) {
                case self::WS_OPCODE_CLOSE:
                    //close
                    $obj->closeClient($client);
                    $event->stopPropagation();
                    return;
            
                case self::WS_OPCODE_PING:
                    //ping
                    return;
            
                case self::WS_OPCODE_PONG:
                    //pong
                    return;
            
                case self::WS_OPCODE_TEXT:
                case self::WS_OPCODE_BIN:
                    $event->setData($unmaskData);
                    break;
            
                default:
                    $event->stopPropagation();
                    return;
            }
        });
        
        return parent::listen(); // TODO: Change the autogenerated stub
    }
    
    /**
     * 解析websocket帧
     * @param $frameData
     * @return array
     */
    protected function _parseFrame($frameData) {
        $result = [
            'fin' => $this->_getBit($frameData[0], 0, 1),
            'rsv1' => $this->_getBit($frameData[0], 1, 1),
            'rsv2' => $this->_getBit($frameData[0], 2, 1),
            'rsv3' => $this->_getBit($frameData[0], 3, 1),
            'opcode' => $this->_getBit($frameData[0], 4, 4),
            'mask' => $this->_getBit($frameData[1], 0, 1),
            'len' => $this->_getBit($frameData[1], 1, 7),
        ];
        
        $len = $result['len'];
        $mask = $result['mask'];
        
        if($len == 126) {
            $length = $this->_toInt(substr($frameData, 2, 2));
            $charIndex = 4;
        } else if($len == 127) {
            $length = $this->_toInt(substr($frameData, 2, 8));
            $charIndex = 10;
        } else {
            $length = $len;
            $charIndex = 2;
        }
        
        $result['length'] = $length;
        
        if($mask) {
            $result['masking_key'] = [ord($frameData[$charIndex]), ord($frameData[1+$charIndex]), ord($frameData[2+$charIndex]), ord($frameData[3+$charIndex])];
            $charIndex = $charIndex + 4;
        }
        
        $data = substr($frameData, $charIndex, $length);
        $result['data'] = $data;
        return $result;
    }
    
    protected function unmaskFrameData($data, $mask, $maskingKey) {
        if(!$mask || empty($maskingKey)) {
            return $data;
        }
        
        $count = strlen($data);
        
        for ($i=0; $i<$count; $i++) {
            $ch = $data[$i];
            $data[$i] = chr(ord($ch) ^ $maskingKey[$i % 4]);
        }
        
        return $data;
    }
    
    protected function _getBit($char, $begin=0, $length=1) {
        $int = ord($char);
        
        $length = $length > (8-$begin) ? (8-$begin) : $length;
        $length = $length <= 0 ? 1 : $length;
        $offset = 8-$begin-$length;
        
        return ($int & 0xFF) >> $offset & (pow(2, $length)-1);
    }
    
    protected function _toInt($str) {
        $val = '';
        $ary = str_split($str, 1);
        
        foreach($ary as $i => $arg) {
            $val .= str_pad(base_convert(ord($arg), 10, 16), 2, '0', STR_PAD_LEFT);
        }
        
        return (int)base_convert($val, 16, 10);
    }
    
    protected function _parseHeaderString($str) {
        $data = [];
        list($headStr, $bodyStr) = explode("\n\n", $str, 2);
        list($methodStr, $headerStr) = explode("\n", $headStr, 2);
        list($method, $path, $protocol) = explode(' ', $methodStr);
        
        $data['method'] = strtoupper(trim($method));
        $data['path'] = trim($path);
        $data['protocol'] = strtoupper(trim($protocol));
        
        $header = [];
        foreach (array_filter(array_map('trim', explode("\n", $headerStr))) as $kvstr) {
            list($key, $value) = explode(':', $kvstr, 2);
            $key = trim($key);
            $value = trim($value);
            
            $header [$key] = $value;
            $header [strtoupper($key)] = $value;
        }
        
        $data['header'] = $header;
        $data['body'] = $bodyStr;
        return $data;
    }
    
    protected function _generateHandshakeResponse($websocketKey) {
        return "HTTP/1.1 101 Switching Protocols\r\n".
                "Upgrade: websocket\r\n".
                "Connection: Upgrade\r\n".
                "Sec-WebSocket-Accept: {$this->_generateAcceptKey($websocketKey)}\r\n\r\n";
    }
    
    protected function _generateAcceptKey($websocketKey) {
        return base64_encode(sha1("{$websocketKey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));
    }
    
    public static function debugAsBin($chars) {
        $str = '';
        
        foreach(str_split($chars, 1) as $char) {
            $str .= str_pad(base_convert(ord($char), 10, 2), 8, '0', STR_PAD_LEFT);
        }
        
        return implode(',', str_split($str, 8));
    }
    
    public function send($data)
    {
        return $this->sendRaw($this->_packFrame($data));
    }
    
    public function sendRaw($data)
    {
        return parent::send($data);
    }
    
    /**
     * 创建websocket帧
     * @param $data
     * @param int $opcode
     * @param int $fin
     * @param int $mask
     * @return string
     */
    protected function _packFrame($data, $opcode=self::WS_OPCODE_TEXT, $fin=1, $mask=0)
    {
        $dword = strval((int)(bool)$fin) . '000' . str_pad(base_convert($opcode & 0xF, 16, 2), 4, '0', STR_PAD_LEFT);
        $dword .= strval((int)(bool)$mask);
    
        $length = strlen($data);
        $payloadStr = '';
        
        if($length < 125) {
            $len = $length;
        } else if($length <= 65535) {
            $len = 126;
            $payloadStr = str_pad(base_convert($length, 10, 2), 16, '0', STR_PAD_LEFT);
        } else {
            $len = 127;
            $payloadStr = str_pad(base_convert($length, 10, 2), 64, '0', STR_PAD_LEFT);
        }
    
        $dword .= str_pad(base_convert($len, 10, 2), 7, '0', STR_PAD_LEFT);
        $dword .= $payloadStr;
        
        if($mask) {
            $maskingKey = [mt_rand(1,254), mt_rand(1,254), mt_rand(1,254), mt_rand(1,254)];
            $dword .= str_pad(base_convert($maskingKey[0], 10, 2), 8, '0', STR_PAD_LEFT);
            $dword .= str_pad(base_convert($maskingKey[1], 10, 2), 8, '0', STR_PAD_LEFT);
            $dword .= str_pad(base_convert($maskingKey[2], 10, 2), 8, '0', STR_PAD_LEFT);
            $dword .= str_pad(base_convert($maskingKey[3], 10, 2), 8, '0', STR_PAD_LEFT);
            
            for($i=0; $i<$length; $i++) {
                $data[$i] = chr(ord($data[$i]) ^ $maskingKey[$i % 4]);
            }
        }
        
        $newDWord = '';
        
        foreach(str_split($dword, 8) as $ch) {
            $newDWord .= chr(intval(base_convert($ch, 2, 10)));
        }
        
        return $newDWord . $data;
    }
}